﻿using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

namespace InfiniTec.IO
{
	/// <summary>
	/// Implements a <see cref="Stream"/> object that reads/writes data through a memory mapping.
	/// </summary>
	public sealed class MemoryMappedFileStream : Stream
	{
		public static long PageFileUsage
		{
			get { return MemoryMappedFileSafeHandle.PageFileUsage; }
		}

		private static readonly int AllocationGranularity;
		private readonly MemoryMappedFileStreamSharedState _SharedState;
		private long _Position;


		private readonly bool _IsReadOnly;

		public long Capacity
		{
			get { return _SharedState.Capacity; }
		}

		public MemoryMappedFileStream Clone()
		{
			var result = new MemoryMappedFileStream(_SharedState);

			return result;
		}

		static MemoryMappedFileStream()
		{
			var systemInfo = new SYSTEM_INFO();
			SafeNativeMethods.GetSystemInfo(ref systemInfo);

			AllocationGranularity = (int)systemInfo.dwAllocationGranularity;
		}

		public MemoryMappedFileStream(string name, string file, FileAccess fileAccess)
		{
			if (string.IsNullOrEmpty(file)) throw new ArgumentNullException("file");

			uint protectionFlags;

			switch (fileAccess)
			{
				case FileAccess.Read:
					protectionFlags = (uint)FileProtection.Readonly;
					break;
				case FileAccess.ReadWrite:
					protectionFlags = (uint)FileProtection.ReadWrite;
					break;
				case FileAccess.Write:
					protectionFlags = (uint)FileProtection.ReadWrite;
					break;
				default:
					throw new InvalidEnumArgumentException("fileAccess", (int)fileAccess, typeof(FileAccess));
			}

			var underlyingStream = new FileStream(file, FileMode.Open, fileAccess, FileShare.None);
			var handle = SafeNativeMethods.CreateFileMapping(underlyingStream.SafeFileHandle, IntPtr.Zero,
														  protectionFlags, 0, 0, string.IsNullOrEmpty(name) ? null : "Local\\" + name);

			_SharedState = new MemoryMappedFileStreamSharedState(handle, underlyingStream.Length, underlyingStream.Length,
																 underlyingStream, this);

		}

		private MemoryMappedFileStream(string name, long size)
		{
			uint loSize;
			uint hiSize;

			ConvertLong(size, out hiSize, out loSize);

			var handle = SafeNativeMethods.CreateFileMapping(new SafeFileHandle(new IntPtr(-1), false),
														  IntPtr.Zero, (uint)FileProtection.ReadWrite,
														  hiSize, loSize, string.IsNullOrEmpty(name) ? null : "Local\\" + name);

			if (handle.IsInvalid)
			{
				var lastWin32Error = (Win32ErrorCode)Marshal.GetLastWin32Error();
				if (lastWin32Error == Win32ErrorCode.PageFileMemoryExceeded)
				{
					throw new InsufficientMemoryException("There is not enough memory in the pagefile available to complete the operation.");
				}
				throw new Win32Exception((int)lastWin32Error);
			}
			if (string.IsNullOrEmpty(name))
			{
				handle.SizeInPageFile = size;
			}
			_SharedState = new MemoryMappedFileStreamSharedState(handle, 0, size, null, this);
		}

		private MemoryMappedFileStream(MemoryMappedFileStreamSharedState sharedState)
		{
			if (sharedState == null) throw new ArgumentNullException("sharedState");
			_SharedState = sharedState;
			_SharedState.AddReference();
			_IsReadOnly = true;
		}

		public override bool CanRead { get { return true; } }

		public override bool CanSeek { get { return true; } }

		public override bool CanWrite { get { return !_IsReadOnly; } }

		public override long Length { get { return _SharedState.Length; } }

		public override long Position
		{
			get { return _Position; }
			set
			{
				if (value < 0 || value > _SharedState.Length)
				{
					throw new ArgumentOutOfRangeException("value", value, "The Position must be between 0 and Length");
				}

				_Position = value;
			}
		}

		/// <summary>
		/// Creates temporary memory mapped file that is backed by the page file.
		/// </summary>
		/// <param name="size">The maximum size of the file.</param>
		/// <returns></returns>
		public static MemoryMappedFileStream CreatePageFileBackedFile(long size)
		{
			return new MemoryMappedFileStream(null, size);
		}

		public override void Flush() { }

		protected override void Dispose(bool disposing)
		{
			base.Dispose(disposing);

			_SharedState.RemoveReference();
		}

		private static void ConvertLong(long value, out uint hiUint, out uint loUint)
		{
			hiUint = (uint)(value >> 32) & uint.MaxValue;
			loUint = (uint)value & uint.MaxValue;
		}

		public override int Read(byte[] buffer, int offset, int count)
		{
			if (buffer == null) throw new ArgumentNullException("buffer");

			if (offset < 0 || offset > buffer.Length)
			{
				throw new ArgumentOutOfRangeException("offset", "Offset out of range.");
			}
			if (offset + count > buffer.Length)
			{
				throw new ArgumentOutOfRangeException("buffer", "The buffer is not big enough for the requested data.");
			}

			IntPtr ptr = IntPtr.Zero;

			try
			{
				if (_Position + count > _SharedState.Length)
				{
					count = (int)(_SharedState.Length - _Position);
				}

				if (count < 1)
				{
					return 0;
				}

				int destinationOffset;
				MapView(_Position, count, FileMapAccesses.Read, out ptr, out destinationOffset);
				var offsetPtr = new IntPtr(((long)ptr) + destinationOffset);

				Marshal.Copy(offsetPtr, buffer, offset, count);

				_Position += count;

				return count;
			}
			finally
			{
				UnmapView(ptr);
			}
		}

		public override long Seek(long offset, SeekOrigin origin)
		{
			long newPosition;

			switch (origin)
			{
				case SeekOrigin.Begin:
					newPosition = offset;
					break;
				case SeekOrigin.Current:
					newPosition = _Position + offset;
					break;
				case SeekOrigin.End:
					newPosition = _SharedState.Length + offset;
					break;
				default:
					throw new InvalidEnumArgumentException("origin", (int)origin, typeof(SeekOrigin));
			}

			if (newPosition < 0 || newPosition > Length)
			{
				throw new ArgumentOutOfRangeException("offset", newPosition, "The new position is out of range.");
			}
			_Position = newPosition;

			return newPosition;
		}

		public override void SetLength(long value)
		{
			if (value < 0 || value > _SharedState.Capacity)
			{
				throw new ArgumentOutOfRangeException("value", value, string.Format("The length must be between 0 and {0}", value));
			}
			_SharedState.SetLength(value, this);
		}

		public override void Write(byte[] buffer, int offset, int count)
		{
			if (buffer == null) throw new ArgumentNullException("buffer");

			if (offset < 0 || offset > buffer.Length)
			{
				throw new ArgumentOutOfRangeException("offset", "Offset out of range.");
			}
			if (offset + count > buffer.Length)
			{
				throw new ArgumentOutOfRangeException("buffer", "The specified range exceeds the size of the buffer.");
			}
			if (Position + count > _SharedState.Capacity)
			{
				throw new ArgumentOutOfRangeException("count", "Cannot write beyond the size of the file.");
			}

			IntPtr ptr = IntPtr.Zero;

			try
			{
				int destinationOffset;
				MapView(_Position, count, FileMapAccesses.Write, out ptr, out destinationOffset);
				var offsetPtr = new IntPtr(((long)ptr) + destinationOffset);

				Marshal.Copy(buffer, offset, offsetPtr, count);

				_Position += count;
				if (_Position > _SharedState.Length)
				{
					_SharedState.SetLength(_Position, this);
				}
			}
			finally
			{
				UnmapView(ptr);
			}
		}

		private static void UnmapView(IntPtr ptr)
		{
			if (ptr != IntPtr.Zero)
			{
				SafeNativeMethods.UnmapViewOfFile(ptr);
			}
		}

		private void MapView(long offset, int count, FileMapAccesses desiredAccess, out IntPtr destinationAddress, out int destinationOffset)
		{
			uint loPos;
			uint hiPos;

			destinationOffset = (int)(offset % AllocationGranularity);
			offset -= destinationOffset;

			ConvertLong(offset, out hiPos, out loPos);
			destinationAddress = SafeNativeMethods.MapViewOfFile(_SharedState.Handle, desiredAccess, hiPos, loPos, (uint)(count + destinationOffset));

			if (destinationAddress == IntPtr.Zero)
			{
				throw new Win32Exception(Marshal.GetLastWin32Error());
			}
		}

		public void CopyEntireContentsTo(MemoryMappedFileStream destination)
		{
			if (destination == null) throw new ArgumentNullException("destination");

			if (destination._SharedState.Capacity < _SharedState.Capacity)
			{
				throw new ArgumentException("The size of the target stream must be equal or greater than the size of the source stream.", "destination");
			}

			int offset = 0;

			do
			{
				var count = (int)Math.Min(Length - offset, 10*AllocationGranularity);
				IntPtr sourcePtr = IntPtr.Zero;
				IntPtr destinationPtr = IntPtr.Zero;

				if (count == 0) break;
				try
				{
					int destinationOffset;
					MapView(offset, count, FileMapAccesses.Read, out sourcePtr, out destinationOffset);
					destination.MapView(offset, count, FileMapAccesses.Write, out destinationPtr, out destinationOffset);

					SafeNativeMethods.CopyMemory(destinationPtr, sourcePtr, new IntPtr(count));
					offset += count;
				}
				finally
				{
					UnmapView(sourcePtr);
					UnmapView(destinationPtr);
				}
			} while (true);

			destination._SharedState.SetLength(_SharedState.Length, destination);
			destination._Position = 0;
		}

		public void Clear()
		{
			int offset = 0;

			do
			{
				var count = (int)Math.Min(Length - offset, AllocationGranularity);
				IntPtr sourcePtr = IntPtr.Zero;

				if (count == 0) break;
				try
				{
					int destinationOffset;
					MapView(offset, count, FileMapAccesses.Write, out sourcePtr, out destinationOffset);

					UnsafeZeroMemory(sourcePtr, count);
					offset += count;
				}
				finally
				{
					UnmapView(sourcePtr);
				}
			} while (true);

			_SharedState.SetLength(0, this);
			_Position = 0;
		}

		private static void UnsafeZeroMemory(IntPtr handle, long count)
		{
			unsafe
			{
				var ptr = (byte*)handle.ToPointer();
				for (int i = 0; i < count; i++)
				{
					*(ptr + i) = 0;
				}
			}
		}

		public void EnsureMemoryIsBlank(int offset, int count)
		{
			IntPtr ptr = IntPtr.Zero;


			if (offset < 0 || offset > _SharedState.Capacity)
			{
				throw new ArgumentOutOfRangeException("offset", "Offset out of range.");
			}
			if (count < 0)
			{
				throw new ArgumentOutOfRangeException("count", "count must be greater than or equal to 0");
			}
			if (offset + count > _SharedState.Capacity)
			{
				throw new ArgumentOutOfRangeException("count", "The specified range exceeds the size of the buffer.");
			}


			try
			{
				int destinationOffset;
				MapView(_Position, count, FileMapAccesses.Read, out ptr, out destinationOffset);
				var offsetPtr = new IntPtr(((long)ptr) + destinationOffset);

				var buffer = new byte[count];
				Marshal.Copy(offsetPtr, buffer, offset, count);

				for (int i = 0; i < count; i++)
				{
					if (buffer[i] != 0) throw new InvalidDataException(string.Format("The stream buffer has not been cleared at offset {0}", i));
				}
			}
			finally
			{
				UnmapView(ptr);
			}
		}
	}
}